/**
 * $Id: IrcConnection.cs 77 2004-09-19 13:31:53Z meebey $
 * $URL: svn://svn.qnetp.net/smartirc/IrcClient/tags/0.2.0/src/IrcConnection.cs $
 * $Rev: 77 $
 * $Author: meebey $
 * $Date: 2004-09-19 15:31:53 +0200 (Sun, 19 Sep 2004) $
 *
 * Copyright (c) 2003-2004 Mirco 'meebey' Bauer <mail@meebey.net> <http://www.meebey.net>
 * 
 * Full LGPL License: <http://www.gnu.org/licenses/lgpl.txt>
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

using System;
using System.Collections;
using System.ComponentModel;
using System.IO;
using System.Net;
using System.Reflection;
using System.Text;
using System.Threading;
using C5;
using MedSoft.Library.Logging;
using MedSoft.Library.Utilities;
using UrDnd.IrcClient.Constants;
using UrDnd.IrcClient.Events;

namespace UrDnd.IrcClient
{
    /// <summary>
    ///
    /// </summary>
    [Log]
    public class IrcConnection
    {
        private int _currentAddress;
        private StreamReader _reader;
        private StreamWriter _writer;
        private readonly ReadThread _readThread;
        private readonly WriteThread _writeThread;
        private IrcTcpClient _tcpClient;
        private readonly IntervalHeap<MessageData> _sendBuffer;
        private int _connectTries;
        private bool _autoRetry;
        private bool _autoReconnect;
        private bool _connectionError;
        private readonly object _lockObject = new object();

        public event EventHandler<IOLineEventArgs> LineRead;
        public event EventHandler<IOLineEventArgs> LineWrote;

        public event EventHandler<CancelEventArgs> Connecting;
        public event EventHandler<EventArgs> Connected;

        public event EventHandler<CancelEventArgs> Disconnecting;
        public event EventHandler<EventArgs> Disconnected;

        private struct MessageData : IComparable<MessageData>, IEquatable<MessageData>
        {
            public readonly string Data;
            public readonly Priority Priority;

            public MessageData(string data, Priority priority)
            {
                Data = data;
                Priority = priority;
            }

            public int CompareTo(MessageData other)
            {
                return Priority - other.Priority;
            }

            public bool Equals(MessageData other)
            {
                return Priority == other.Priority;
            }
        }

        protected bool ConnectionError
        {
            get
            {
                lock (_lockObject)
                    return _connectionError;
            }
            set
            {
                lock (_lockObject)
                    _connectionError = value;
            }
        }

        public string Address
        {
            get { return AddressList[_currentAddress]; }
        }

        public string[] AddressList { get; private set; }

        public int Port { get; private set; }

        public bool AutoReconnect
        {
            get { return _autoReconnect; }
            set
            {
                Singleton<Logger>.Instance.Info(value ? "AutoReconnect enabled" : "AutoReconnect disabled");
                _autoReconnect = value;
            }
        }

        public bool AutoRetry
        {
            get { return _autoRetry; }
            set
            {
                Singleton<Logger>.Instance.Info(value ? "AutoRetry enabled" : "AutoRetry disabled");
                _autoRetry = value;
            }
        }

        public int SendDelay { get; set; }

        public bool IsRegistered { get; private set; }

        public bool IsConnected { get; private set; }

        public string Version { get; private set; }

        public string VersionString { get; private set; }

        public Encoding Encoding { get; set; }

        public IrcConnection()
        {
            _sendBuffer = new IntervalHeap<MessageData>();
            Port = 6667;
            Encoding = Encoding.UTF8;
            _connectionError = false;
            SendDelay = 200;
            AddressList = new[] {"localhost"};

            LineRead += SimpleParser;

            _readThread = new ReadThread(this);
            _writeThread = new WriteThread(this);

            var assembly = Assembly.GetAssembly(GetType());
            var assemblyName = assembly.GetName(false);

            var pr =
                (AssemblyProductAttribute) assembly.GetCustomAttributes(typeof (AssemblyProductAttribute), false)[0];

            Version = assemblyName.Version.ToString();
            VersionString = string.Format("{0} {1}", pr.Product, Version);
        }

        public bool Connect(string[] addresslist, int port)
        {
            if (IsConnected)
                throw new InvalidOperationException("Already connected");


            Singleton<Logger>.Instance.Info("Connecting");

            _connectTries++;
            AddressList = (string[]) addresslist.Clone();
            Port = port;

            if (!OnConnecting())
                return false;

            try
            {
                var ip = Dns.GetHostEntry(Address).AddressList[0];
                _tcpClient = new IrcTcpClient();
                _tcpClient.Connect(ip, port);
                OnConnected();

                _reader = new StreamReader(_tcpClient.GetStream(), Encoding);
                _writer = new StreamWriter(_tcpClient.GetStream(), Encoding);

                // Connection was succeful, reseting the connect counter
                _connectTries = 0;

                // updating the connection id, so connecting is possible again
                lock (_lockObject)
                    ConnectionError = false;

                IsConnected = true;

                Singleton<Logger>.Instance.Info("Connected");

                // lets power up our threads
                _readThread.Start();
                _writeThread.Start();
                return true;
            }
            catch (Exception ex)
            {
                if (_reader != null) _reader.Close();
                if (_writer != null) _writer.Close();
                if (_tcpClient != null) _tcpClient.Close();
                IsConnected = false;
                ConnectionError = true;

                Singleton<Logger>.Instance.Info("Connection failed.", ex);

                if (_autoRetry && _connectTries <= 3)
                {
                    NextAddress();
                    if (Reconnect(false)) return true;
                }

                return false;
            }
        }

        protected virtual void OnConnected()
        {
            FireEvent(Connected, EventArgs.Empty);
        }

        protected virtual bool OnConnecting()
        {
            return FireCancelableEvent(Connecting);
        }

        protected bool FireCancelableEvent(EventHandler<CancelEventArgs> @event)
        {
            var args = new CancelEventArgs();

            FireEvent(@event, args);

            return !args.Cancel;
        }

        public bool Connect(string address, int port)
        {
            return Connect(new[] {address}, port);
        }

        // login parameter only for IrcClient needed
        public virtual bool Reconnect(bool login)
        {
            Singleton<Logger>.Instance.Info("Reconnecting...");

            Disconnect();
            return Connect(AddressList, Port);
        }

        public bool Reconnect()
        {
            return Reconnect(true);
        }

        public bool Disconnect()
        {
            if (!IsConnected)
                return false;


            if (!OnDisconnecting())
                return false;

            Singleton<Logger>.Instance.Info("Disconnecting...");
            _readThread.Stop();
            _writeThread.Stop();
            _tcpClient.Close();
            IsConnected = false;
            IsRegistered = false;

            OnDisconnected();

            Singleton<Logger>.Instance.Info("Disconnected");

            return true;
        }

        protected virtual void OnDisconnected()
        {
            FireEvent(Disconnected, EventArgs.Empty);
        }

        protected virtual bool OnDisconnecting()
        {
            return FireCancelableEvent(Disconnecting);
        }

        public void Listen(bool blocking)
        {
            if (blocking)
            {
                while (IsConnected)
                {
                    ReadLine(true);
                    if (ConnectionError)
                        if (AutoReconnect)
                            Reconnect();
                        else
                            Disconnect();
                }
            }
            else
            {
                while (!string.IsNullOrEmpty(ReadLine(false)))
                {
                    // loop as long as we receive messages
                }
            }
        }

        public void Listen()
        {
            Listen(true);
        }

        public void ListenOnce(bool blocking)
        {
            ReadLine(blocking);
        }

        public void ListenOnce()
        {
            ListenOnce(true);
        }

        public string ReadLine(bool blocking)
        {
            var data = "";

            if (blocking)
            {
                // block till the queue has data
                while (IsConnected && (_readThread.Queue.Count == 0))
                    Thread.Sleep(10);
            }

            if (IsConnected && (_readThread.Queue.Count > 0))
                data = (string) _readThread.Queue.Dequeue();

            if (!string.IsNullOrEmpty(data))
            {
                Singleton<Logger>.Instance.DebugFormat("Read: \"{0}\"", data);
                OnLineRead(new IOLineEventArgs(data));
            }

            return data;
        }

        protected virtual void OnLineRead(IOLineEventArgs args)
        {
            FireEvent(LineRead, args);
        }

        protected void FireEvent<T>(EventHandler<T> @event, T args)
            where T : EventArgs
        {
            if (@event != null)
                @event(this, args);
        }

        public void WriteLine(string data, Priority priority)
        {
            lock (_lockObject)
                _sendBuffer.Add(new MessageData(data, priority));
        }

        public void WriteLine(string data)
        {
            WriteLine(data, Priority.Medium);
        }

        private bool WriteLineInternal(string data)
        {
            if (IsConnected)
            {
                try
                {
                    Singleton<Logger>.Instance.DebugFormat("Sending: \"{0}\"", data);
                    _writer.Write(data + "\r\n");
                    _writer.Flush();
                    Singleton<Logger>.Instance.DebugFormat("Sent: \"{0}\"", data);
                }
                catch (IOException)
                {
                    Singleton<Logger>.Instance.Warn("Sending data failed, connection lost");
                    ConnectionError = true;
                    return false;
                }

                OnLineWrote(new IOLineEventArgs(data));
                return true;
            }

            return false;
        }

        protected virtual void OnLineWrote(IOLineEventArgs args)
        {
            FireEvent(LineWrote, args);
        }

        private void NextAddress()
        {
            _currentAddress++;
            if (_currentAddress >= AddressList.Length)
                _currentAddress = 0;

            Singleton<Logger>.Instance.InfoFormat("Set server to: {0}", Address);
        }

        private void SimpleParser(object sender, IOLineEventArgs args)
        {
            var rawLineEx = args.RawLine.Split(new[] {' '});

            if (args.RawLine.Substring(0, 1) == ":")
            {
                try
                {
                    var replycode = (ReplyCode) int.Parse(rawLineEx[1]);
                    if (replycode == ReplyCode.RPL_WELCOME)
                    {
                        IsRegistered = true;
                        Singleton<Logger>.Instance.Info("Logged in");
                    }
                }
                catch (FormatException)
                {
                    // this is not login answer
                }
            }
            else
            {
                if (rawLineEx[0] == "ERROR")
                {
                    ConnectionError = true;
                }
            }
        }

        [Log]
        private class ReadThread
        {
            private readonly IrcConnection _connection;
            private Thread _thread;
            // syncronized queue (thread safe)
            private readonly Queue _queue = Queue.Synchronized(new Queue());

            public Queue Queue
            {
                get { return _queue; }
            }

            public ReadThread(IrcConnection connection)
            {
                _connection = connection;
            }

            public void Start()
            {
                _thread = new Thread(Worker)
                              {
                                  Name = string.Format("ReadThread ({0}:{1})", _connection.Address, _connection.Port),
                                  IsBackground = true
                              };
                _thread.Start();
            }

            public void Stop()
            {
                _thread.Abort();
                _connection._reader.Close();
            }

            private void Worker()
            {
                Singleton<Logger>.Instance.DebugFormat("ReadThread '{0}' started", _thread.Name);

                try
                {
                    try
                    {
                        string data;
                        while (_connection.IsConnected &&
                               ((data = _connection._reader.ReadLine()) != null))
                        {
                            _queue.Enqueue(data);

                            Singleton<Logger>.Instance.Debug("received: \"" + data + "\"");
                        }
                    }
                    catch (IOException ex)
                    {
                        Singleton<Logger>.Instance.Warn("Error occured while reading data", ex);
                    }

                    Singleton<Logger>.Instance.Warn("Connection lost");
                    _connection.ConnectionError = true;
                }
                catch (ThreadAbortException)
                {
                    Thread.ResetAbort();
                    Singleton<Logger>.Instance.DebugFormat("ReadThread {0} aborted", _thread.Name);
                }
            }
        }

        [Log]
        private class WriteThread
        {
            private readonly IrcConnection _connection;
            private Thread _thread;

            public WriteThread(IrcConnection connection)
            {
                _connection = connection;
            }

            public void Start()
            {
                _thread = new Thread(Worker)
                              {
                                  Name = string.Format("WriteThread ({0}:{1})", _connection.Address, _connection.Port),
                                  IsBackground = true
                              };
                _thread.Start();
            }

            public void Stop()
            {
                _thread.Abort();
                _connection._writer.Close();
            }

            private void Worker()
            {
                Singleton<Logger>.Instance.DebugFormat("WriteThread '{0}' started", _thread.Name);

                try
                {
                    while (_connection.IsConnected)
                    {
                    	if (!_connection._sendBuffer.IsEmpty)
                    	{
                    		MessageData toSend;
                    		lock (_connection._lockObject)
                    			toSend = _connection._sendBuffer.DeleteMax();

                    		if (!_connection.WriteLineInternal(toSend.Data))
                    			lock (_connection._lockObject)
                    				_connection._sendBuffer.Add(toSend);
                    	}
                        Thread.Sleep(_connection.SendDelay);
                    }

                    Singleton<Logger>.Instance.Warn("connection lost");
                    _connection.ConnectionError = true;
                }
                catch (ThreadAbortException)
                {
                    Thread.ResetAbort();
                    Singleton<Logger>.Instance.DebugFormat("WriteThread '{0}' aborted", _thread.Name);
                }
            }
        }
    }
}